/******************************************************************************
 *
 * Project:  GRC/GRD Reader
 * Purpose:  Northwood Format basic implementation
 * Author:   Perry Casson
 *
 ******************************************************************************
 * Copyright (c) 2007, Waypoint Information Technology
 * Copyright (c) 2009-2011, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "gdal_pam.h"

#include "northwood.h"

#include <algorithm>
#include <cassert>
#include <limits>
#include <string>

int nwt_ParseHeader(NWT_GRID *pGrd, const unsigned char *nwtHeader)
{
    /* double dfTmp; */

    if (nwtHeader[4] == '1')
        pGrd->cFormat = 0x00;  // grd - surface type
    else if (nwtHeader[4] == '8')
        pGrd->cFormat = 0x80;  //  grc classified type

    pGrd->stClassDict = nullptr;

    memcpy(&pGrd->fVersion, &nwtHeader[5], sizeof(pGrd->fVersion));
    CPL_LSBPTR32(&pGrd->fVersion);

    unsigned short usTmp;
    memcpy(&usTmp, &nwtHeader[9], 2);
    CPL_LSBPTR16(&usTmp);
    pGrd->nXSide = static_cast<unsigned int>(usTmp);
    if (pGrd->nXSide == 0)
    {
        memcpy(&pGrd->nXSide, &nwtHeader[128], sizeof(pGrd->nXSide));
        CPL_LSBPTR32(&pGrd->nXSide);
    }
    if (pGrd->nXSide <= 1)
        return FALSE;

    memcpy(&usTmp, &nwtHeader[11], 2);
    CPL_LSBPTR16(&usTmp);
    pGrd->nYSide = static_cast<unsigned int>(usTmp);
    if (pGrd->nYSide == 0)
    {
        memcpy(&pGrd->nYSide, &nwtHeader[132], sizeof(pGrd->nYSide));
        CPL_LSBPTR32(&pGrd->nYSide);
    }

    memcpy(&pGrd->dfMinX, &nwtHeader[13], sizeof(pGrd->dfMinX));
    CPL_LSBPTR64(&pGrd->dfMinX);
    memcpy(&pGrd->dfMaxX, &nwtHeader[21], sizeof(pGrd->dfMaxX));
    CPL_LSBPTR64(&pGrd->dfMaxX);
    memcpy(&pGrd->dfMinY, &nwtHeader[29], sizeof(pGrd->dfMinY));
    CPL_LSBPTR64(&pGrd->dfMinY);
    memcpy(&pGrd->dfMaxY, &nwtHeader[37], sizeof(pGrd->dfMaxY));
    CPL_LSBPTR64(&pGrd->dfMaxY);

    assert(pGrd->nXSide > 1);
    pGrd->dfStepSize = (pGrd->dfMaxX - pGrd->dfMinX) / (pGrd->nXSide - 1);
    /* dfTmp = (pGrd->dfMaxY - pGrd->dfMinY) / (pGrd->nYSide - 1); */

    memcpy(&pGrd->fZMin, &nwtHeader[45], sizeof(pGrd->fZMin));
    CPL_LSBPTR32(&pGrd->fZMin);
    memcpy(&pGrd->fZMax, &nwtHeader[49], sizeof(pGrd->fZMax));
    CPL_LSBPTR32(&pGrd->fZMax);
    memcpy(&pGrd->fZMinScale, &nwtHeader[53], sizeof(pGrd->fZMinScale));
    CPL_LSBPTR32(&pGrd->fZMinScale);
    memcpy(&pGrd->fZMaxScale, &nwtHeader[57], sizeof(pGrd->fZMaxScale));
    CPL_LSBPTR32(&pGrd->fZMaxScale);

    memcpy(&pGrd->cDescription, &nwtHeader[61], sizeof(pGrd->cDescription));
    memcpy(&pGrd->cZUnits, &nwtHeader[93], sizeof(pGrd->cZUnits));

    int i;
    memcpy(&i, &nwtHeader[136], 4);
    CPL_LSBPTR32(&i);

    if (i == 1129336130)
    {  // BMPC
        if (nwtHeader[140] & 0x01)
        {
            pGrd->cHillShadeBrightness = nwtHeader[144];
            pGrd->cHillShadeContrast = nwtHeader[145];
        }
    }

    memcpy(&pGrd->cMICoordSys, &nwtHeader[256], sizeof(pGrd->cMICoordSys));
    pGrd->cMICoordSys[sizeof(pGrd->cMICoordSys) - 1] = '\0';

    pGrd->iZUnits = nwtHeader[512];

    if (nwtHeader[513] & 0x80)
        pGrd->bShowGradient = true;

    if (nwtHeader[513] & 0x40)
        pGrd->bShowHillShade = true;

    if (nwtHeader[513] & 0x20)
        pGrd->bHillShadeExists = true;

    memcpy(&pGrd->iNumColorInflections, &nwtHeader[516], 2);
    CPL_LSBPTR16(&pGrd->iNumColorInflections);

    if (pGrd->iNumColorInflections > 32)
    {
        CPLError(CE_Failure, CPLE_AppDefined, "Corrupt header");
        pGrd->iNumColorInflections = 0;
        return FALSE;
    }

    for (i = 0; i < pGrd->iNumColorInflections; i++)
    {
        memcpy(&pGrd->stInflection[i].zVal, &nwtHeader[518 + (7 * i)], 4);
        CPL_LSBPTR32(&pGrd->stInflection[i].zVal);
        memcpy(&pGrd->stInflection[i].r, &nwtHeader[522 + (7 * i)], 1);
        memcpy(&pGrd->stInflection[i].g, &nwtHeader[523 + (7 * i)], 1);
        memcpy(&pGrd->stInflection[i].b, &nwtHeader[524 + (7 * i)], 1);
    }

    memcpy(&pGrd->fHillShadeAzimuth, &nwtHeader[966],
           sizeof(pGrd->fHillShadeAzimuth));
    CPL_LSBPTR32(&pGrd->fHillShadeAzimuth);
    memcpy(&pGrd->fHillShadeAngle, &nwtHeader[970],
           sizeof(pGrd->fHillShadeAngle));
    CPL_LSBPTR32(&pGrd->fHillShadeAngle);

    pGrd->cFormat += nwtHeader[1023];  // the msb for grd/grc was already set

    // there are more types than this - need to build other types for testing
    if (pGrd->cFormat & 0x80)
    {
        if (nwtHeader[1023] == 0)
            pGrd->nBitsPerPixel = 16;
        else
            pGrd->nBitsPerPixel = nwtHeader[1023] * 4;
    }
    else
        pGrd->nBitsPerPixel = nwtHeader[1023] * 8;

    if (pGrd->cFormat & 0x80)  // if is GRC load the Dictionary
    {
        vsi_l_offset nPixels =
            static_cast<vsi_l_offset>(pGrd->nXSide) * pGrd->nYSide;
        unsigned int nBytesPerPixel = pGrd->nBitsPerPixel / 8;
        if (nPixels > 0 &&
            (nBytesPerPixel >
                 std::numeric_limits<vsi_l_offset>::max() / nPixels ||
             nPixels * nBytesPerPixel >
                 std::numeric_limits<vsi_l_offset>::max() - 1024))
        {
            CPLError(CE_Failure, CPLE_FileIO,
                     "Invalid file dimension / bits per pixel");
            return FALSE;
        }
        VSIFSeekL(pGrd->fp, 1024 + nPixels * nBytesPerPixel, SEEK_SET);

        if (!VSIFReadL(&usTmp, 2, 1, pGrd->fp))
        {
            CPLError(CE_Failure, CPLE_FileIO, "Read failure, file short?");
            return FALSE;
        }
        CPL_LSBPTR16(&usTmp);
        pGrd->stClassDict = reinterpret_cast<NWT_CLASSIFIED_DICT *>(
            calloc(1, sizeof(NWT_CLASSIFIED_DICT)));
        if (!pGrd->stClassDict)
        {
            return FALSE;
        }

        pGrd->stClassDict->nNumClassifiedItems = usTmp;

        pGrd->stClassDict->stClassifiedItem =
            reinterpret_cast<NWT_CLASSIFIED_ITEM **>(
                calloc(pGrd->stClassDict->nNumClassifiedItems + 1,
                       sizeof(NWT_CLASSIFIED_ITEM *)));
        if (!pGrd->stClassDict->stClassifiedItem)
        {
            pGrd->stClassDict->nNumClassifiedItems = 0;
            return FALSE;
        }

        // load the dictionary
        for (unsigned int iItem = 0;
             iItem < pGrd->stClassDict->nNumClassifiedItems; iItem++)
        {
            NWT_CLASSIFIED_ITEM *psItem =
                pGrd->stClassDict->stClassifiedItem[iItem] =
                    reinterpret_cast<NWT_CLASSIFIED_ITEM *>(
                        calloc(1, sizeof(NWT_CLASSIFIED_ITEM)));
            if (!psItem)
            {
                return FALSE;
            }

            unsigned char cTmp[256];
            if (!VSIFReadL(&cTmp, 9, 1, pGrd->fp))
            {
                CPLError(CE_Failure, CPLE_FileIO, "Read failure, file short?");
                return FALSE;
            }
            memcpy(&psItem->usPixVal, &cTmp[0], 2);
            CPL_LSBPTR16(&psItem->usPixVal);
            memcpy(&psItem->res1, &cTmp[2], 1);
            memcpy(&psItem->r, &cTmp[3], 1);
            memcpy(&psItem->g, &cTmp[4], 1);
            memcpy(&psItem->b, &cTmp[5], 1);
            memcpy(&psItem->res2, &cTmp[6], 1);
            memcpy(&psItem->usLen, &cTmp[7], 2);
            CPL_LSBPTR16(&psItem->usLen);

            if (psItem->usLen > sizeof(psItem->szClassName) - 1)
            {
                CPLError(CE_Failure, CPLE_AppDefined,
                         "Unexpected long class name, %d characters long - "
                         "unable to read file.",
                         psItem->usLen);
                return FALSE;
            }

            // 0-len class names are possible
            psItem->szClassName[0] = '\0';
            if (psItem->usLen > 0 &&
                !VSIFReadL(&psItem->szClassName, psItem->usLen, 1, pGrd->fp))
                return FALSE;
        }
    }

    return TRUE;
}

// Create a color gradient ranging from ZMin to Zmax using the color
// inflections defined in grid
int nwt_LoadColors(NWT_RGB *pMap, int mapSize, NWT_GRID *pGrd)
{
    int i;
    NWT_RGB sColor;
    int nWarkerMark = 0;

    createIP(0, 255, 255, 255, pMap, &nWarkerMark);
    if (pGrd->iNumColorInflections == 0)
        return 0;

    // If Zmin is less than the 1st inflection use the 1st inflections color to
    // the start of the ramp
    if (pGrd->fZMin <= pGrd->stInflection[0].zVal)
    {
        createIP(1, pGrd->stInflection[0].r, pGrd->stInflection[0].g,
                 pGrd->stInflection[0].b, pMap, &nWarkerMark);
    }
    // find what inflections zmin is between
    for (i = 1; i < pGrd->iNumColorInflections; i++)
    {
        if (pGrd->fZMin < pGrd->stInflection[i].zVal)
        {
            // then we must be between i and i-1
            linearColor(&sColor, &pGrd->stInflection[i - 1],
                        &pGrd->stInflection[i], pGrd->fZMin);
            createIP(1, sColor.r, sColor.g, sColor.b, pMap, &nWarkerMark);
            break;
        }
    }
    // the interesting case of zmin beig higher than the max inflection value
    if (i >= pGrd->iNumColorInflections)
    {
        createIP(1, pGrd->stInflection[pGrd->iNumColorInflections - 1].r,
                 pGrd->stInflection[pGrd->iNumColorInflections - 1].g,
                 pGrd->stInflection[pGrd->iNumColorInflections - 1].b, pMap,
                 &nWarkerMark);
        createIP(mapSize - 1,
                 pGrd->stInflection[pGrd->iNumColorInflections - 1].r,
                 pGrd->stInflection[pGrd->iNumColorInflections - 1].g,
                 pGrd->stInflection[pGrd->iNumColorInflections - 1].b, pMap,
                 &nWarkerMark);
    }
    else
    {
        int index = 0;
        for (; i < pGrd->iNumColorInflections; i++)
        {
            if (pGrd->fZMax < pGrd->stInflection[i].zVal)
            {
                // then we must be between i and i-1
                linearColor(&sColor, &pGrd->stInflection[i - 1],
                            &pGrd->stInflection[i], pGrd->fZMax);
                index = mapSize - 1;
                createIP(index, sColor.r, sColor.g, sColor.b, pMap,
                         &nWarkerMark);
                break;
            }
            // save the inflections between zmin and zmax
            index =
                static_cast<int>(((pGrd->stInflection[i].zVal - pGrd->fZMin) /
                                  (pGrd->fZMax - pGrd->fZMin)) *
                                 mapSize);

            if (index >= mapSize)
                index = mapSize - 1;
            createIP(index, pGrd->stInflection[i].r, pGrd->stInflection[i].g,
                     pGrd->stInflection[i].b, pMap, &nWarkerMark);
        }
        if (index < mapSize - 1)
            createIP(mapSize - 1,
                     pGrd->stInflection[pGrd->iNumColorInflections - 1].r,
                     pGrd->stInflection[pGrd->iNumColorInflections - 1].g,
                     pGrd->stInflection[pGrd->iNumColorInflections - 1].b, pMap,
                     &nWarkerMark);
    }
    return 0;
}

// solve for a color between pIPLow and pIPHigh
void linearColor(NWT_RGB *pRGB, NWT_INFLECTION *pIPLow, NWT_INFLECTION *pIPHigh,
                 float fMid)
{
    if (fMid < pIPLow->zVal)
    {
        pRGB->r = pIPLow->r;
        pRGB->g = pIPLow->g;
        pRGB->b = pIPLow->b;
    }
    else if (fMid > pIPHigh->zVal)
    {
        pRGB->r = pIPHigh->r;
        pRGB->g = pIPHigh->g;
        pRGB->b = pIPHigh->b;
    }
    else
    {
        float scale = (fMid - pIPLow->zVal) / (pIPHigh->zVal - pIPLow->zVal);
        pRGB->r = static_cast<unsigned char>(scale * (pIPHigh->r - pIPLow->r) +
                                             pIPLow->r + 0.5);
        pRGB->g = static_cast<unsigned char>(scale * (pIPHigh->g - pIPLow->g) +
                                             pIPLow->g + 0.5);
        pRGB->b = static_cast<unsigned char>(scale * (pIPHigh->b - pIPLow->b) +
                                             pIPLow->b + 0.5);
    }
}

// insert IP's into the map filling as we go
void createIP(int index, unsigned char r, unsigned char g, unsigned char b,
              NWT_RGB *map, int *pnWarkerMark)
{
    int i;

    if (index == 0)
    {
        map[0].r = r;
        map[0].g = g;
        map[0].b = b;
        *pnWarkerMark = 0;
        return;
    }

    if (index <= *pnWarkerMark)
        return;

    int wm = *pnWarkerMark;

    float rslope =
        static_cast<float>(r - map[wm].r) / static_cast<float>(index - wm);
    float gslope =
        static_cast<float>(g - map[wm].g) / static_cast<float>(index - wm);
    float bslope =
        static_cast<float>(b - map[wm].b) / static_cast<float>(index - wm);
    for (i = wm + 1; i < index; i++)
    {
        map[i].r =
            static_cast<unsigned char>(map[wm].r + ((i - wm) * rslope) + 0.5);
        map[i].g =
            static_cast<unsigned char>(map[wm].g + ((i - wm) * gslope) + 0.5);
        map[i].b =
            static_cast<unsigned char>(map[wm].b + ((i - wm) * bslope) + 0.5);
    }
    map[index].r = r;
    map[index].g = g;
    map[index].b = b;
    *pnWarkerMark = index;
    return;
}

void nwt_HillShade(unsigned char *r, unsigned char *g, unsigned char *b,
                   unsigned char *h)
{
    HLS hls;
    NWT_RGB rgb;
    rgb.r = *r;
    rgb.g = *g;
    rgb.b = *b;
    hls = RGBtoHLS(rgb);
    hls.l = static_cast<short>(hls.l + (*h) * HLSMAX / 256);
    rgb = HLStoRGB(hls);

    *r = rgb.r;
    *g = rgb.g;
    *b = rgb.b;
    return;
}

NWT_GRID *nwtOpenGrid(char *filename)
{
    unsigned char nwtHeader[1024];
    VSILFILE *fp = VSIFOpenL(filename, "rb");

    if (fp == nullptr)
    {
        CPLError(CE_Failure, CPLE_OpenFailed, "Can't open %s", filename);
        return nullptr;
    }

    if (!VSIFReadL(nwtHeader, 1024, 1, fp))
    {
        VSIFCloseL(fp);
        return nullptr;
    }

    if (nwtHeader[0] != 'H' || nwtHeader[1] != 'G' || nwtHeader[2] != 'P' ||
        nwtHeader[3] != 'C')
    {
        VSIFCloseL(fp);
        return nullptr;
    }

    NWT_GRID *pGrd = reinterpret_cast<NWT_GRID *>(calloc(1, sizeof(NWT_GRID)));
    if (!pGrd)
    {
        VSIFCloseL(fp);
        return nullptr;
    }

    if (nwtHeader[4] == '1')
        pGrd->cFormat = 0x00;  // grd - surface type
    else if (nwtHeader[4] == '8')
        pGrd->cFormat = 0x80;  //  grc classified type
    else
    {
        CPLError(CE_Failure, CPLE_NotSupported,
                 "Unhandled Northwood format type = %0xd", nwtHeader[4]);
        VSIFCloseL(fp);
        free(pGrd);
        return nullptr;
    }

    strncpy(pGrd->szFileName, filename, sizeof(pGrd->szFileName));
    pGrd->szFileName[sizeof(pGrd->szFileName) - 1] = '\0';
    pGrd->fp = fp;
    nwt_ParseHeader(pGrd, nwtHeader);

    return pGrd;
}

// close the file and free the mem
void nwtCloseGrid(NWT_GRID *pGrd)
{
    if ((pGrd->cFormat & 0x80) &&
        pGrd->stClassDict)  // if is GRC - free the Dictionary
    {
        for (unsigned int i = 0; i < pGrd->stClassDict->nNumClassifiedItems;
             i++)
        {
            free(pGrd->stClassDict->stClassifiedItem[i]);
        }
        free(pGrd->stClassDict->stClassifiedItem);
        free(pGrd->stClassDict);
    }
    if (pGrd->fp)
        VSIFCloseL(pGrd->fp);
    free(pGrd);
    return;
}

void nwtPrintGridHeader(NWT_GRID *pGrd)
{
    if (pGrd->cFormat & 0x80)
    {
        printf("\n%s\n\nGrid type is Classified ", pGrd->szFileName); /*ok*/
        if (pGrd->cFormat == 0x81)
            printf("4 bit (Less than 16 Classes)"); /*ok*/
        else if (pGrd->cFormat == 0x82)
            printf("8 bit (Less than 256 Classes)"); /*ok*/
        else if (pGrd->cFormat == 0x84)
            printf("16 bit (Less than 65536 Classes)"); /*ok*/
        else
        {
            printf("GRC - Unhandled Format or Type %d", pGrd->cFormat); /*ok*/
            return;
        }
    }
    else
    {
        printf("\n%s\n\nGrid type is Numeric ", pGrd->szFileName); /*ok*/
        if (pGrd->cFormat == 0x00)
            printf("16 bit (Standard Precision)"); /*ok*/
        else if (pGrd->cFormat == 0x01)
            printf("32 bit (High Precision)"); /*ok*/
        else
        {
            printf("GRD - Unhandled Format or Type %d", pGrd->cFormat); /*ok*/
            return;
        }
    }
    printf("\nDim (x,y) = (%u,%u)", pGrd->nXSide, pGrd->nYSide);     /*ok*/
    printf("\nStep Size = %f", pGrd->dfStepSize);                    /*ok*/
    printf("\nBounds = (%f,%f) (%f,%f)", pGrd->dfMinX, pGrd->dfMinY, /*ok*/
           pGrd->dfMaxX, pGrd->dfMaxY);
    printf("\nCoordinate System = %s", pGrd->cMICoordSys); /*ok*/

    if (!(pGrd->cFormat & 0x80))  // print the numeric specific stuff
    {
        printf("\nMin Z = %f Max Z = %f Z Units = %d \"%s\"", /*ok*/
               pGrd->fZMin, pGrd->fZMax, pGrd->iZUnits, pGrd->cZUnits);

        printf("\n\nDisplay Mode ="); /*ok*/
        if (pGrd->bShowGradient)
            printf(" Color Gradient"); /*ok*/

        if (pGrd->bShowGradient && pGrd->bShowHillShade)
            printf(" and"); /*ok*/

        if (pGrd->bShowHillShade)
            printf(" Hill Shading"); /*ok*/

        for (int i = 0; i < pGrd->iNumColorInflections; i++)
        {
            printf("\nColor Inflection %d - %f (%d,%d,%d)", i + 1, /*ok*/
                   pGrd->stInflection[i].zVal, pGrd->stInflection[i].r,
                   pGrd->stInflection[i].g, pGrd->stInflection[i].b);
        }

        if (pGrd->bHillShadeExists)
        {
            printf("\n\nHill Shade Azumith = %.1f Inclination = %.1f " /*ok*/
                   "Brightness = %d Contrast = %d",
                   pGrd->fHillShadeAzimuth, pGrd->fHillShadeAngle,
                   pGrd->cHillShadeBrightness, pGrd->cHillShadeContrast);
        }
        else
            printf("\n\nNo Hill Shade Data"); /*ok*/
    }
    else  // print the classified specific stuff
    {
        printf("\nNumber of Classes defined = %u", /*ok*/
               pGrd->stClassDict->nNumClassifiedItems);
        for (int i = 0;
             i < static_cast<int>(pGrd->stClassDict->nNumClassifiedItems); i++)
        {
            printf("\n%s - (%d,%d,%d)  Raw = %d  %d %d", /*ok*/
                   pGrd->stClassDict->stClassifiedItem[i]->szClassName,
                   pGrd->stClassDict->stClassifiedItem[i]->r,
                   pGrd->stClassDict->stClassifiedItem[i]->g,
                   pGrd->stClassDict->stClassifiedItem[i]->b,
                   pGrd->stClassDict->stClassifiedItem[i]->usPixVal,
                   pGrd->stClassDict->stClassifiedItem[i]->res1,
                   pGrd->stClassDict->stClassifiedItem[i]->res2);
        }
    }
}

HLS RGBtoHLS(NWT_RGB rgb)
{
    /* get R, G, and B out of DWORD */
    short R = rgb.r;
    short G = rgb.g;
    short B = rgb.b;

    /* calculate lightness */
    unsigned char cMax =
        static_cast<unsigned char>(std::max(std::max(R, G), B));
    unsigned char cMin =
        static_cast<unsigned char>(std::min(std::min(R, G), B));
    HLS hls;
    hls.l = (((cMax + cMin) * HLSMAX) + RGBMAX) / (2 * RGBMAX);

    short Rdelta, Gdelta, Bdelta; /* intermediate value: % of spread from max */
    if (cMax == cMin)
    {                      /* r=g=b --> achromatic case */
        hls.s = 0;         /* saturation */
        hls.h = UNDEFINED; /* hue */
    }
    else
    { /* chromatic case */
        /* saturation */
        if (hls.l <= (HLSMAX / 2))
            hls.s = (((cMax - cMin) * HLSMAX) + ((cMax + cMin) / 2)) /
                    (cMax + cMin);
        else
            hls.s =
                (((cMax - cMin) * HLSMAX) + ((2 * RGBMAX - cMax - cMin) / 2)) /
                (2 * RGBMAX - cMax - cMin);

        /* hue */
        Rdelta =
            (((cMax - R) * (HLSMAX / 6)) + ((cMax - cMin) / 2)) / (cMax - cMin);
        Gdelta =
            (((cMax - G) * (HLSMAX / 6)) + ((cMax - cMin) / 2)) / (cMax - cMin);
        Bdelta =
            (((cMax - B) * (HLSMAX / 6)) + ((cMax - cMin) / 2)) / (cMax - cMin);

        if (R == cMax)
            hls.h = Bdelta - Gdelta;
        else if (G == cMax)
            hls.h = (HLSMAX / 3) + Rdelta - Bdelta;
        else /* B == cMax */
            hls.h = ((2 * HLSMAX) / 3) + Gdelta - Rdelta;

        if (hls.h < 0)
            hls.h += HLSMAX;
        if (hls.h > HLSMAX)
            hls.h -= HLSMAX;
    }
    return hls;
}

/* utility routine for HLStoRGB */
static short HueToRGB(short n1, short n2, short hue)
{
    /* range check: note values passed add/subtract thirds of range */
    if (hue < 0)
        hue += HLSMAX;

    if (hue > HLSMAX)
        hue -= HLSMAX;

    /* return r,g, or b value from this tridrant */
    if (hue < (HLSMAX / 6))
        return n1 + (((n2 - n1) * hue + (HLSMAX / 12)) / (HLSMAX / 6));
    if (hue < (HLSMAX / 2))
        return n2;
    if (hue < ((HLSMAX * 2) / 3))
        return n1 + (((n2 - n1) * (((HLSMAX * 2) / 3) - hue) + (HLSMAX / 12)) /
                     (HLSMAX / 6));
    else
        return n1;
}

NWT_RGB HLStoRGB(HLS hls)
{
    NWT_RGB rgb;

    if (hls.s == 0)
    { /* achromatic case */
        rgb.r = static_cast<unsigned char>((hls.l * RGBMAX) / HLSMAX);
        rgb.g = rgb.r;
        rgb.b = rgb.r;
        if (hls.h != UNDEFINED)
        {
            /* ERROR */
        }
    }
    else
    { /* chromatic case */
        /* set up magic numbers */
        short Magic1, Magic2; /* calculated magic numbers (really!) */
        if (hls.l <= (HLSMAX / 2))
            Magic2 = (hls.l * (HLSMAX + hls.s) + (HLSMAX / 2)) / HLSMAX;
        else
            Magic2 = hls.l + hls.s - ((hls.l * hls.s) + (HLSMAX / 2)) / HLSMAX;
        Magic1 = 2 * hls.l - Magic2;

        /* get RGB, change units from HLSMAX to RGBMAX */
        rgb.r = static_cast<unsigned char>(
            (HueToRGB(Magic1, Magic2, hls.h + (HLSMAX / 3)) * RGBMAX +
             (HLSMAX / 2)) /
            HLSMAX);
        rgb.g = static_cast<unsigned char>(
            (HueToRGB(Magic1, Magic2, hls.h) * RGBMAX + (HLSMAX / 2)) / HLSMAX);
        rgb.b = static_cast<unsigned char>(
            (HueToRGB(Magic1, Magic2, hls.h - (HLSMAX / 3)) * RGBMAX +
             (HLSMAX / 2)) /
            HLSMAX);
    }

    return rgb;
}
